package edu.ucsd.db.bassarisk.algebra.logical.visitor;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

import edu.ucsd.db.bassarisk.algebra.logical.AggregateAll;
import edu.ucsd.db.bassarisk.algebra.logical.AggregateWithin;
import edu.ucsd.db.bassarisk.algebra.logical.ConstantTuple;
import edu.ucsd.db.bassarisk.algebra.logical.Intersection;
import edu.ucsd.db.bassarisk.algebra.logical.Join;
import edu.ucsd.db.bassarisk.algebra.logical.JoinWithin;
import edu.ucsd.db.bassarisk.algebra.logical.LogicalOperator;
import edu.ucsd.db.bassarisk.algebra.logical.Map;
import edu.ucsd.db.bassarisk.algebra.logical.Scan;
import edu.ucsd.db.bassarisk.algebra.logical.Select;
import edu.ucsd.db.bassarisk.algebra.logical.Union;
import edu.ucsd.db.bassarisk.algebra.logical.WithinContext;
import edu.ucsd.db.bassarisk.query.binding.Bindings.Binding;

public class LogicalPlanPrinter implements
    LogicalOperatorVisitor<Void, StringBuilder> {
  private static final String indent = "  "; // two spaces
  private static final String ws = " "; // one space
  private int indentDepth;

  public LogicalPlanPrinter(int baseIndent) {
    this.indentDepth = baseIndent;
  }

  private void newLine(StringBuilder sb) {
    sb.append("\n").append(StringUtils.repeat(indent, indentDepth));
  }

  private StringBuilder printInBlock(LogicalOperator op, StringBuilder sb) {
    sb.append("{");
    this.indentDepth++;
    this.newLine(sb);
    op.accept(this, sb);
    this.indentDepth--;
    this.newLine(sb);
    sb.append("}");
    return sb;
  }

  private StringBuilder
      printInBlock(Iterable<LogicalOperator> ops, StringBuilder sb) {
    sb.append("{");
    this.indentDepth++;
    boolean first = true;
    for (LogicalOperator op : ops) {
      if (first) {
        first = false;
      } else {
        sb.append(",");
      }
      this.newLine(sb);
      op.accept(this, sb);
    }
    this.indentDepth--;
    this.newLine(sb);
    sb.append("}");
    return sb;
  }

  @Override
  public Void visitAggregateAll(AggregateAll op, StringBuilder sb) {
    this.printName("AggregateAll", op, sb);
    this.printArgs(op.aggregates(), sb);
    this.printInBlock(op.subPlan(), sb);
    return null;
  }

  private void printName(String name, LogicalOperator op, StringBuilder sb) {
    sb.append(name);
    if (op.getOutputDesciptor() != null) {
      List<Integer> bindingIds = new ArrayList<Integer>();
      for (Binding b : op.getOutputDesciptor().bindings()) {
        bindingIds.add(b.getId());
      }
      sb.append("<B");
      sb.append(StringUtils.join(bindingIds, ", B"));
      sb.append(">");
    }
  }

  private void printArgs(Iterable<? extends Object> args, StringBuilder sb) {
    sb.append("[");
    sb.append(StringUtils.join(args, ", "));
    sb.append("]");
  }

  @Override
  public Void visitAggregateWithin(AggregateWithin op, StringBuilder sb) {
    this.printName("AggregateWithin", op, sb);
    this.printArgs(op.aggregates(), sb);
    this.printInBlock(op.subPlan(), sb);
    return null;
  }

  @Override
  public Void visitConstantTuple(ConstantTuple op, StringBuilder sb) {
    this.printName("Tuple", op, sb);
    this.printArgs(op.fields(), sb);
    return null;
  }

  @Override
  public Void visitIntersection(Intersection op, StringBuilder sb) {
    this.printName("Intersection", op, sb);
    this.printInBlock(op.intersectionPlans(), sb);
    return null;
  }

  @Override
  public Void visitJoin(Join op, StringBuilder sb) {
    this.printName("Join", op, sb);
    this.printInBlock(op.joinInputs(), sb);
    return null;
  }

  @Override
  public Void visitJoinWithin(JoinWithin op, StringBuilder sb) {
    this.printName("JoinWithin", op, sb);
    this.printInBlock(op.joinInputs(), sb);
    return null;
  }

  @Override
  public Void visitMap(Map op, StringBuilder sb) {
    this.printName("Map", op, sb);
    this.printArgs(op.functions(), sb);
    this.printInBlock(op.subPlan(), sb);
    return null;
  }

  @Override
  public Void visitScan(Scan op, StringBuilder sb) {
    this.printName("Scan", op, sb);
    String format = "[%s, %s, %s]";
    sb.append(String.format(format, op.getColumnScanned(), op.filterValue(),
                            op.getBindingSatisfied()));
    return null;
  }

  @Override
  public Void visitUnion(Union op, StringBuilder sb) {
    this.printName("Union", op, sb);
    this.printInBlock(op.unionPlans(), sb);
    return null;
  }

  @Override
  public Void visitWithinContext(WithinContext op, StringBuilder sb) {
    this.printName("Within", op, sb);
    this.printInBlock(op.filterPlan(), sb).append(ws);
    this.printInBlock(op.subPlan(), sb);
    return null;
  }

  @Override
  public Void visitSelect(Select op, StringBuilder sb) {
    this.printName("Select", op, sb);
    this.printArgs(op.predicates(), sb);
    this.printInBlock(op.subPlan(), sb);
    return null;
  }
}
